﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Security.Cryptography;
using Microsoft.Cryptography;

namespace Microsoft.Cryptography.BouncyCastle
{
    /// <summary>
    /// The default RSACryptoServiceProvider class can't handle RSA-signatures, which
    /// are using SHA-2 or RIPEMD-160 as hash-algorithms. Even if the method
    /// VerifyHash is used and the correct calculated hash is given as parameter an
    /// error occurs.
    /// To circumvent these problems use this class, which implements the RSA algorithm
    /// managed and is using RSACryptoServiceProvider only if possible (for encryption,
    /// decryption and signature with SHA-1 and MD5)
    /// </summary>
    public sealed class RSABouncyCastle : RSABase
    {
        private readonly Org.BouncyCastle.Crypto.IAsymmetricBlockCipher rsaEngine = new Org.BouncyCastle.Crypto.Encodings.Pkcs1Encoding(new Org.BouncyCastle.Crypto.Engines.RsaBlindedEngine());

        #region constructors

        /// <summary>
        /// RSA Managed standard constructor.
        /// </summary>
        public RSABouncyCastle()
        {
        }

        /// <summary>
        /// RSAManged Constructor.
        /// </summary>
        /// <param name="rsaParameters">RSAParameter to import.</param>
        public RSABouncyCastle(RSAParameters rsaParameters)
        {
            ImportParameters(rsaParameters);
        }

        #endregion

        #region private fields

        /// <summary>
        /// Current imported RSA-Parameters.
        /// </summary>
        private Nullable<RSAParameters> _rsaParams;
        private RSACryptoServiceProvider _rsaCSP = null;

        #endregion

        /// <summary>
        /// RSA-Parameters, which should be used
        /// </summary>
        public Nullable<RSAParameters> RSAParameters
        {
            get { return _rsaParams; }
        }

        private RSACryptoServiceProvider CryptoServiceProvider
        {
            get
            {
                if (_rsaCSP == null)
                {
                    lock (this)
                    {
                        if (_rsaCSP == null)
                        {
                            if (_rsaParams == null)
                            {
                                throw new ArgumentNullException("RSAParameters");
                            }

                            _rsaCSP = new RSACryptoServiceProvider();
                            _rsaCSP.ImportParameters(_rsaParams.Value);
                        }
                    }
                }

                return _rsaCSP;
            }
        }


        /// <summary>
        /// Decrypt the given byte-array by using the currently imported RSA-Parameters.
        /// </summary>
        /// <param name="rgb">Data to decrypt as byte array.</param>
        /// <returns></returns>
        public override byte[] DecryptValue(byte[] rgb)
        {
            throw new NotSupportedException();

            //// Check the data against null.
            //if (rgb == null)
            //{
            //    throw new ArgumentNullException("rgb");
            //}
            //if (_rsaParams == null)
            //{
            //    throw new Exception(ErrorText.RSAParamsNotSet);
            //}
            //// Make sure that we have sufficient RSA parameters.
            //if (_rsaParams.Value.Modulus == null)
            //{
            //    throw new Exception(ErrorText.RSAParamsNotSet);
            //}

            //return CryptoServiceProvider.Decrypt(rgb, false);
        }

        /// <summary>
        /// Encrypt the given byte-array by using the currently imported RSA-Parameters.
        /// </summary>
        /// <param name="rgb">Data to encrypt as byte array.</param>
        /// <returns></returns>
        public override byte[] EncryptValue(byte[] rgb)
        {
            throw new NotSupportedException();

            //// Check the data against null.
            //if (rgb == null)
            //{
            //    throw new ArgumentNullException("rgb");
            //}
            //if (_rsaParams == null)
            //{
            //    throw new Exception(ErrorText.RSAParamsNotSet);
            //}
            //// Make sure that we have sufficient RSA parameters.
            //if (_rsaParams.Value.Modulus == null)
            //{
            //    throw new Exception(ErrorText.RSAParamsNotSet);
            //}

            //return CryptoServiceProvider.Encrypt(rgb, false);
        }

        /// <summary>
        /// Overridden. Exports the RSAParameters.
        /// </summary>
        /// <param name="includePrivateParameters">true to include private parameters; otherwise, false. </param>
        /// <returns>The parameters for RSA.</returns>
        public override RSAParameters ExportParameters(bool includePrivateParameters)
        {
            if (_rsaParams == null)
            {
                throw new ArgumentNullException("_rsaParameters");
            }
            if (_rsaParams.Value.Modulus == null)
            {
                throw new Exception(ErrorText.RSAParamsNotSet);
            }

            if (includePrivateParameters == true)
            {
                return _rsaParams.Value;
            }
            else
            {
                RSAParameters paramWithoutPrivate = new RSAParameters();
                paramWithoutPrivate.Modulus = _rsaParams.Value.Modulus;
                paramWithoutPrivate.Exponent = _rsaParams.Value.Exponent;
                paramWithoutPrivate.D = null;
                paramWithoutPrivate.DP = null;
                paramWithoutPrivate.DQ = null;
                paramWithoutPrivate.InverseQ = null;
                paramWithoutPrivate.P = null;
                paramWithoutPrivate.Q = null;
                return paramWithoutPrivate;
            }
        }

        /// <summary>
        /// Overridden. Imports the specified RSAParameters.
        /// </summary>
        /// <param name="parameters">The parameters for RSA.</param>
        public override void ImportParameters(RSAParameters parameters)
        {
            if (parameters.Modulus == null || parameters.Exponent == null)
            {
                throw new Exception(ErrorText.InvalidRSAParams);
            }

            _rsaParams = parameters;
            _rsaCSP = null;
        }

        /// <summary>
        /// Dispose method.
        /// </summary>
        /// <param name="disposing"></param>
        protected override void Dispose(bool disposing)
        {
        }

        /// <summary>
        /// 
        /// </summary>
        public override string KeyExchangeAlgorithm
        {
            get { return CryptoServiceProvider.KeyExchangeAlgorithm; }
        }

        /// <summary>
        /// 
        /// </summary>
        public override string SignatureAlgorithm
        {
            get 
            { 
                return CryptoServiceProvider.SignatureAlgorithm; 
            }
        }

        /// <summary>
        /// Compute a hash value over a buffer and sign it.
        /// </summary>
        /// <param name="buffer">Buffer to sign.</param>
        /// <param name="halg">Hashalgorithm for signing.</param>
        /// <returns>Signing succeed.</returns>
        public override byte[] SignData(byte[] buffer, object halg)
        {
            string hashName;
            HashAlgorithm alg = ObjectToHashAlg(halg, out hashName);
            byte[] rgbHash = alg.ComputeHash(buffer);
            return SignHash(rgbHash, hashName);
        }

        /// <summary>
        /// Sign an explicit hash value.
        /// </summary>
        /// <param name="rgbHash"></param>
        /// <param name="halg"></param>
        /// <returns>Signing succeed.</returns>
        public override byte[] SignHash(byte[] rgbHash, string halg)
        {
            // Validate the parameters.
            if (rgbHash == null)
            {
                throw new ArgumentNullException("rgbHash");
            }
            if (halg == null)
            {
                throw new ArgumentNullException("halg");
            }

            if (_rsaParams == null)
            {
                throw new Exception(ErrorText.RSAParamsNotSet);
            }

            // Make sure that we have sufficient RSA parameters.
            if (_rsaParams.Value.Modulus == null)
            {
                throw new Exception(ErrorText.RSAParamsNotSet);
            }

            System.Diagnostics.Stopwatch watchDoCrypt = new System.Diagnostics.Stopwatch();
            watchDoCrypt.Start();

            string hashName = "";
            HashAlgorithm hashAlgorithm = ObjectToHashAlg(halg, out hashName);

            Org.BouncyCastle.Crypto.IDigest digest = Org.BouncyCastle.Security.DigestUtilities.GetDigest(BouncyCastleUtilities.DigestIdentifierFroHashAlgorithm(hashAlgorithm));

            Org.BouncyCastle.Asn1.DerObjectIdentifier oid = Org.BouncyCastle.Security.DigestUtilities.GetObjectIdentifier(BouncyCastleUtilities.DigestIdentifierFroHashAlgorithm(hashAlgorithm));
            if (oid == null)
            {
                throw new Exception(string.Format("Could not find OID for hash algorithm '{0}'.", BouncyCastleUtilities.DigestIdentifierFroHashAlgorithm(hashAlgorithm)));
            }

            Org.BouncyCastle.Asn1.X509.DigestInfo digestInfo = new Org.BouncyCastle.Asn1.X509.DigestInfo(new Org.BouncyCastle.Asn1.X509.AlgorithmIdentifier(oid, Org.BouncyCastle.Asn1.DerNull.Instance),
                rgbHash);

            rsaEngine.Init(true, BouncyCastleUtilities.GetRsaKeyPair(_rsaParams.Value).Private);
            byte[] data = digestInfo.GetDerEncoded();
            byte[] signedValue = rsaEngine.ProcessBlock(data, 0, data.Length);

            watchDoCrypt.Stop();
            System.Diagnostics.Trace.WriteLine(string.Format("SignHash.DoCrypt: {0} ms", watchDoCrypt.ElapsedMilliseconds));

            // Done.
            return signedValue;
        }

        
        /// <summary>
        /// Verify the signature on a buffer of data.
        /// </summary>
        /// <param name="buffer"></param>
        /// <param name="halg"></param>
        /// <param name="signatureData"></param>
        /// <returns></returns>
        public override bool VerifyData(byte[] buffer, object halg, byte[] signatureData)
        { 
            System.Diagnostics.Stopwatch watchVerifyData = new System.Diagnostics.Stopwatch();
            watchVerifyData.Start();
            string hashName = "";
            HashAlgorithm alg = ObjectToHashAlg(halg, out hashName);

            System.Diagnostics.Stopwatch watchGetHash = new System.Diagnostics.Stopwatch();
            watchGetHash.Start();
            byte[] rgbHash = alg.ComputeHash(buffer);
            watchGetHash.Stop();

            System.Diagnostics.Stopwatch watchVerifyHash = new System.Diagnostics.Stopwatch();
            watchVerifyHash.Start();
            bool returnValue = VerifyHash(rgbHash, halg, signatureData);
            watchVerifyHash.Stop();
            watchVerifyData.Stop();

            System.Diagnostics.Trace.WriteLine(string.Format("VerifyData: {0} ms", watchVerifyData.ElapsedMilliseconds));
            System.Diagnostics.Trace.WriteLine(string.Format("GetHash: {0} ms", watchGetHash.ElapsedMilliseconds));
            System.Diagnostics.Trace.WriteLine(string.Format("VerifyHash: {0} ms", watchVerifyHash.ElapsedMilliseconds));

            return returnValue;
        }
        
        /// <summary>
        /// Verify a signature from an explicit hash value.
        /// </summary>
        /// <param name="rgbHash"></param>
        /// <param name="halg"></param>
        /// <param name="signatureData"></param>
        /// <returns></returns>
        public override bool VerifyHash(byte[] rgbHash, object halg, byte[] signatureData)
        {
            // Validate the parameters.
            if (rgbHash == null)
            {
                throw new ArgumentNullException("rgbHash");
            }
            if (halg == null)
            {
                throw new ArgumentNullException("halg");
            }
            if (signatureData == null)
            {
                throw new ArgumentNullException("signatureData");
            }

            if (_rsaParams == null)
            {
                throw new Exception(ErrorText.RSAParamsNotSet);
            }

            // Make sure that we have sufficient RSA parameters.
            if (_rsaParams.Value.Modulus == null)
            {
                throw new Exception(ErrorText.RSAParamsNotSet);
            }

            int k = _rsaParams.Value.Modulus.Length;

            if (k < signatureData.Length)
            {
                for (int i = 0; i < signatureData.Length - k; i++)
                {
                    if (signatureData[i] != 0)
                    {
                        throw new CryptographicException("invalid signature");
                    }
                }

                byte[] old = signatureData;
                signatureData = new byte[k];
                Array.Copy(old, old.Length - k, signatureData, 0, k);
            }

            if (k != signatureData.Length)
            {
                throw new CryptographicException("invalid signature");
            }

            System.Diagnostics.Stopwatch watchDoCrypt = new System.Diagnostics.Stopwatch();
            watchDoCrypt.Start();

            string hashName = "";
            HashAlgorithm hashAlgorithm = ObjectToHashAlg(halg, out hashName);

            Org.BouncyCastle.Crypto.IDigest digest = Org.BouncyCastle.Security.DigestUtilities.GetDigest(BouncyCastleUtilities.DigestIdentifierFroHashAlgorithm(hashAlgorithm));

            Org.BouncyCastle.Asn1.DerObjectIdentifier oid = Org.BouncyCastle.Security.DigestUtilities.GetObjectIdentifier(BouncyCastleUtilities.DigestIdentifierFroHashAlgorithm(hashAlgorithm));
            if (oid == null)
            {
                throw new Exception(string.Format("Could not find OID for hash algorithm '{0}'.", digest.AlgorithmName));
            }

            Org.BouncyCastle.Asn1.X509.DigestInfo digestInfo = new Org.BouncyCastle.Asn1.X509.DigestInfo(new Org.BouncyCastle.Asn1.X509.AlgorithmIdentifier(oid, Org.BouncyCastle.Asn1.DerNull.Instance),
                rgbHash);

            rsaEngine.Init(false, BouncyCastleUtilities.GetRsaPublicKey(_rsaParams.Value));
            byte[] expected;
            byte[] signedValue;

            try
            {
                signedValue = rsaEngine.ProcessBlock(signatureData, 0, signatureData.Length);
                expected = digestInfo.GetDerEncoded();
            }
            catch (Exception error)
            {
                System.Diagnostics.Trace.WriteLine(string.Format("Exception: {0}", error.ToString()));
                return false;
            }
            finally
            {
                watchDoCrypt.Stop();
                System.Diagnostics.Trace.WriteLine(string.Format("VerifyHash.DoCrypt: {0} ms", watchDoCrypt.ElapsedMilliseconds));
            }

            if (signedValue.Length == expected.Length)
            {
                for (int i = 0; i < signedValue.Length; i++)
                {
                    if (signedValue[i] != expected[i])
                    {
                        return false;
                    }
                }
            }
            else if (signedValue.Length == expected.Length - 2)  // NULL left out
            {
                int sigOffset = signedValue.Length - rgbHash.Length - 2;
                int expectedOffset = expected.Length - rgbHash.Length - 2;

                expected[1] -= 2;      // adjust lengths
                expected[3] -= 2;

                for (int i = 0; i < rgbHash.Length; i++)
                {
                    if (signedValue[sigOffset + i] != expected[expectedOffset + i])  // check hash
                    {
                        return false;
                    }
                }

                for (int i = 0; i < sigOffset; i++)
                {
                    if (signedValue[i] != expected[i])  // check header less NULL
                    {
                        return false;
                    }
                }
            }
            else
            {
                return false;
            }

            return true;
        }
    }
}
